#include <iostream>
#include <fstream>

#include <string>
#include <map>
#include <vector>

#include <algorithm>

#include <tuple>
#include <utility>
#include <memory> 

struct Chromosome { 
  typedef std::string orf_t;
  typedef std::pair<size_t, size_t> interval_t;
  typedef std::pair<orf_t, std::pair<size_t, interval_t> > gene_t;

  Chromosome () 
  : isCircular(false) 
  { 
  }

  inline void insert(const orf_t& gene, size_t offset, int sign, interval_t const & interval) { 
    auto gen = gene_t(gene, std::make_pair(sign, interval));
    main_chromosome.insert(std::make_pair(offset, gen));
  } 

  inline void do_circular() { 
    isCircular = true; 
  } 

  inline std::map<size_t, gene_t>::const_iterator begin() const { 
    return main_chromosome.cbegin();	
  } 

  inline std::map<size_t, gene_t>::const_iterator end() const { 
    return main_chromosome.cend();
  } 

  inline std::map<size_t, gene_t>::const_reverse_iterator crbegin() const { 
    return main_chromosome.crbegin();	
  } 

  inline std::map<size_t, gene_t>::const_reverse_iterator crend() const { 
    return main_chromosome.crend();
  } 

  inline size_t size() const {
    return main_chromosome.size();      
  } 

  inline bool is_circular() const { 
    return isCircular;
  }
private: 
  bool isCircular; 
  std::map<size_t, gene_t> main_chromosome;
}; 

struct Genome {
  typedef std::string orf_t;
  typedef std::pair<orf_t, int> gene_t;
  typedef Chromosome chromosome_t; 
  
  Genome() {
  } 

  explicit Genome(std::string const & nm) 
  : name(nm) 
  {
  } 

  inline void insert(orf_t const & gene, std::string const & chromosome, size_t offset, int sign) {
    main_genome[chromosome].insert(gene, offset, sign, std::make_pair(offset, offset + 1));
  }   

  inline void insert(orf_t const & gene, std::string const & chromosome, size_t offset, int sign, size_t start, size_t end) {
    main_genome[chromosome].insert(gene, offset, sign, std::make_pair(start, end));
  }   

  inline void insert(std::string const & name_chr, chromosome_t const & chr) {
    main_genome.insert(std::make_pair(name_chr, chr)); 
  } 
	
  inline void registrate_circular_chr(std::string const & name) { 
    main_genome[name].do_circular();
  }  

  inline size_t size() const { 
    size_t sz = 0; 
    std::for_each(main_genome.begin(), main_genome.end(), [&](std::pair<std::string, chromosome_t> const & chromosome) {
      sz += chromosome.second.size(); 
    });
    return sz; 
  }

  inline size_t count_chromosome() const { 
    return main_genome.size();
  } 

  inline std::string const & get_name() const {  
    return name;
  } 

  inline std::map<std::string, chromosome_t>::const_iterator begin() const { 
    return main_genome.cbegin();	
  } 

  inline std::map<std::string, chromosome_t>::const_iterator end() const { 
    return main_genome.cend();
  } 
private: 
  std::string name;
  std::map<std::string, chromosome_t> main_genome;
};

struct OutputBuilder { 
  OutputBuilder(Genome genome) 
  : m_genome(genome) 
  {
  } 

  virtual void write_name_file() { 
  } 

  virtual void write_head_file() {
  } 
  
  virtual void write_comments() {
  } 

  virtual void write_body_file() { 
  } 

  virtual void save() {
    std::ofstream out(m_name_file.c_str()); 
    out << m_body;
    out.close(); 
  } 

  virtual ~OutputBuilder() = 0;

protected:
  Genome m_genome;
  std::string m_name_file;
  std::string m_body;
};

OutputBuilder::~OutputBuilder() {
} 

/*
Write information in grimm format
*/
struct GrimmOutputBuilder : public OutputBuilder { 
  GrimmOutputBuilder(Genome genome) 
  : OutputBuilder(genome)
  {
  } 

  void write_name_file() { 
    m_name_file = m_genome.get_name() + ".txt";
  } 

  void write_head_file() { 
    m_body = "# Reconstructed genome " + m_genome.get_name() + " has " + std::to_string(m_genome.count_chromosome()) + "\n\n";
    m_body += "> " + m_genome.get_name() + "\n"; 
  } 

  void write_body_file() { 
    for(auto const & chromosome : m_genome) {
      if (chromosome.second.begin()->second.second.first == 1 || (--chromosome.second.end())->second.second.first == 1) {
        for(auto const & block : chromosome.second) {
	  m_body += ((block.second.second.first == -1)?"-":"+") + block.second.first + " ";
        }
      } else {
        for(auto ip = chromosome.second.crbegin(); ip != chromosome.second.crend(); ++ip) {
	  m_body += ((ip->second.second.first == -1)?"+":"-") + ip->second.first + " ";
        }
      }

      if (chromosome.second.is_circular()) {			
        m_body += "@\n";
      } else { 
        m_body += "$\n";
      } 
    }
  } 
};

/*
Write information in infercars format
*/
struct InfercarsOutputBuilder : public OutputBuilder {   
  typedef std::tuple<std::string, std::string, size_t, size_t, int> info_t;

  InfercarsOutputBuilder(Genome genome) 
  : OutputBuilder(genome)
  {
  } 

  void write_name_file() { 
    m_name_file = m_genome.get_name() + ".txt";
  }

  void write_head_file() {
    m_body = "# " + m_genome.get_name() + "\n"; 
    m_body += "# Reconstructed genome " + m_genome.get_name() + " has " + std::to_string(m_genome.count_chromosome()) + "\n\n";
  } 
 
  void write_comments() { 
    size_t number_circular = 0; 
    size_t length_circular = 0;

    for(auto const & chromosome : m_genome) {
      if (chromosome.second.is_circular()) {
        ++number_circular;
        length_circular += chromosome.second.size();
        m_body += "# Chromosome " + chromosome.first + " is circular\n";
      } else {
        m_body += "# Chromosome " + chromosome.first + " is linear\n";
      }
 
      for(auto const & block : chromosome.second) {
    	if (block2info.count(block.second.first) == 0) { 
          auto info = info_t(m_genome.get_name(), chromosome.first, block.second.second.second.first, block.second.second.second.second, block.second.second.first);
          block2info.insert(std::make_pair(block.second.first, std::vector<info_t>({info})));
        } else { 
          block2info[block.second.first].push_back(std::make_tuple(m_genome.get_name(), chromosome.first, block.second.second.second.first, block.second.second.second.second, block.second.second.first));
        } 
      } 
    } 

    m_body += "# out of which " + std::to_string(number_circular) + " are circular of total length " + std::to_string(length_circular) + "\n\n";
  } 

  void write_body_file() { 
    for (auto const & block : block2info) {
      m_body += "> " + block.first + "\n";
      for (auto const & info : block.second) { 
        std::string name_genome = "";
        std::string name_chr = ""; 
        size_t start = 0; 
        size_t end = 0;
        size_t sign = 0; 
        std::tie(name_genome, name_chr, start, end, sign) = info;
        m_body += name_genome + "." + name_chr + ":" + std::to_string(start) + "-" + std::to_string(end) + " " + ((sign == -1)?"-":"+") + "\n";
      } 
    } 
  } 

private: 
  std::map<std::string, std::vector<info_t> > block2info;
};

/*
Write information in circos format
*/
struct CircosOutputBuilder : public OutputBuilder { 
  CircosOutputBuilder(Genome genome) 
  : OutputBuilder(genome)
  {
  } 

  void write_name_file() { 
    m_name_file = m_genome.get_name() + ".circos.conf";
  } 

  void save() { 
    std::cout << "Info: Start support in version 2.5" << std::endl;
  } 
}; 

/*
Director
*/
struct Director { 
  void save_in_format(OutputBuilder& builder) { 
    builder.write_name_file();
    builder.write_head_file(); 
    builder.write_comments();
    builder.write_body_file();
    builder.save();        
  } 
}; 

int main() { 
  /*Create tested dataset*/
  Genome genome("rat");
  genome.insert("1", "chr1", 1, 1);
  genome.insert("2", "chr1", 2, -1); 
  genome.insert("3", "chr1", 3, 1);
  genome.insert("4", "chr2", 1, -1);
  genome.registrate_circular_chr("chr2");

  Genome genome1("mouse");
  genome1.insert("1", "chr1", 1, 1);
  genome1.insert("2", "chr1", 2, -1); 
  genome1.insert("3", "chr1", 3, 1);
  genome1.insert("4", "chr2", 1, -1);
  genome1.registrate_circular_chr("chr2");

  /*Tested builders*/
  Director dir;

  /*Grimm builder*/
  /*
  return: text file in grimm format
  */
  GrimmOutputBuilder gob(genome);
  dir.save_in_format(gob);

  /*Infercars builder*/
  /*
  return: text file in infercars format
  */
  InfercarsOutputBuilder iob(genome1);
  dir.save_in_format(iob);
 
  /*Circos builder*/
  /*
  return: None file
  stdout: Info: Start support in version 2.5
  */
  CircosOutputBuilder cob(genome);
  dir.save_in_format(cob);
 
  return 0; 
}
